/**
 * Copyright 2011 Will Harris will@phase.net
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.phase.wallet;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.URI;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.ParseException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.format.DateFormat;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnClickListener;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;

class Currency implements Runnable
{
	public final static int STATUS_SUCCESS = 0;
	public final static int STATUS_ERROR = 1;
	private Handler handler;
	private static long lastChecked = 0;
	private static JSONObject lastResult = null;
	private static long cacheLength = 1000 * 60 * 30; // 30 minutes
	private final static String url = "http://bitcoincharts.com/t/weighted_prices.json";

	public Currency( Handler h )
	{
		this.handler = h;
	}

	public static void updateWeightedCurrencies( ) throws IOException, JSONException
	{
		Date now = new Date();
		
		if ( ( now.getTime() - lastChecked ) > cacheLength )
		{
			HttpClient client = new DefaultHttpClient();
			HttpGet hg = new HttpGet( url );
	
			HttpResponse response = client.execute(hg);
			
			if ( response.getStatusLine().getStatusCode() == HttpStatus.SC_OK )
			{
				lastResult = new JSONObject( EntityUtils.toString( response.getEntity() ) );
				lastChecked = now.getTime();
			}
			else
			{
				WalletActivity.toastMessage("Warning: unable to get currency information");
			}
		}
	}

	public static String [] getAvailableCurrencies( ) throws IOException, JSONException
	{
		String [] result = null;
		ArrayList<String> resAL = new ArrayList<String>();

		updateWeightedCurrencies();

		if ( lastResult != null )
		{
			// JSONObject.keys() returns Iterator<String> but for some reason
			// isn't typed that way
			@SuppressWarnings("unchecked")
			Iterator<String> itr = lastResult.keys();
			
			// look through every transaction
			while ( itr.hasNext() )
			{
				resAL.add( itr.next() );
			}
		}

		if ( resAL.size() > 0 )
		{
			result = new String [ resAL.size() ];
			resAL.toArray( result );
			Arrays.sort( result );
		}

		return result;
	}
	
	public static double getRate( String currency )
	{
		double result = 0;

		if ( lastResult != null )
		{
			try
			{
				// JSONObject.keys() returns Iterator<String> but for some reason
				// isn't typed that way
				@SuppressWarnings("unchecked")
				Iterator<String> itr = lastResult.keys();
				
				// look through every transaction
				while ( itr.hasNext() )
				{
					String cur = itr.next();
					if ( cur.equals( currency ) )
					{
						JSONObject currencyObject = lastResult.getJSONObject( cur );
						try
						{
							result = currencyObject.getDouble( "30d" );
							result = currencyObject.getDouble( "7d" );
							result = currencyObject.getDouble( "24h" );
						}
						catch (JSONException e)
						{
							// caught when the first failure occurs above
						}
					}
				}
			}
			catch (JSONException e)
			{

			}
		}
		
		return result;
	}

	@Override
	public void run()
	{
		try
		{
			updateWeightedCurrencies( );
			handler.sendEmptyMessage( STATUS_SUCCESS );
		}
		catch ( IOException e )
		{
			handler.sendEmptyMessage( STATUS_ERROR );
		}
		catch ( JSONException e )
		{
			handler.sendEmptyMessage( STATUS_ERROR );
		}
	}
}

class Transaction implements Parcelable, Comparable<Transaction>
{
	public Date date;
	public long amount;
	public long amountin;
	public long amountout;
	public String from;
	public String to;
	public long balance; // used for rolling balance

	protected static Transaction [] compressTransactions( Transaction [] transactions )
	{
		ArrayList<Transaction> txs = new ArrayList<Transaction>();
		Date currentDate = new Date();
		Transaction currentTransaction = null;
		
		for ( Transaction transaction : transactions )
		{
			if ( !currentDate.equals( transaction.date ) )
			{
				currentTransaction = new Transaction( transaction.date, 0, transaction.from, transaction.to );
				currentTransaction.addTransaction( transaction );
				txs.add( currentTransaction );
				currentDate = transaction.date;
			}
			else
			{
				// same date, just add the data
				if ( currentTransaction != null )
				{
					currentTransaction.addTransaction( transaction );
				}
			}
		}

		Transaction [] result = new Transaction[ txs.size() ];
		txs.toArray( result );
		
		return result;	
	}

	protected static Date latest( Transaction [] transactions )
	{
		Date result = null;

		if ( transactions != null && transactions.length > 0 )
		{
			for (Transaction t : transactions)
			{
				if (result == null || result.before( t.date ) )
				{
					result = t.date;
				}
			}
		}

		return result;
	}

	private void addTransaction( Transaction tx )
	{
		this.amount += tx.amount;
		
		if ( amount >= 0 )
		{
			this.amountin += tx.amount;
		}
		else
		{
			this.to = tx.to;
			this.amountout -= tx.amount;
		}
	}
	protected void saveTransaction( PrintWriter out ) throws IOException
	{
		out.println( date.getTime() );
		out.println( amount );
		out.println( from );
		out.println( to );
	}

	protected Transaction( BufferedReader in ) throws NumberFormatException, IOException
	{
		this.date = WalletActivity.myParseDate( in.readLine() );
		this.amount = Long.parseLong( in.readLine() );
		this.from = in.readLine();
		this.to = in.readLine();
	}

	public Transaction( Date date, long amount, String from, String to )
	{
		this.date = date;
		this.amount = amount;
		this.from = from;
		this.to = to;
	}

	public Transaction( Parcel in )
	{
		this.date = (Date) in.readSerializable();
		this.amount = in.readLong();
		this.from = in.readString();
		this.to = in.readString();
		this.amountin = in.readLong();
		this.amountout = in.readLong();
	}

	@Override
	public int describeContents()
	{
		return 0;
	}

	@Override
	public void writeToParcel(Parcel dest, int flags)
	{
		dest.writeSerializable( this.date );
		dest.writeLong( amount );
		dest.writeString( from );
		dest.writeString( to );
		dest.writeLong( amountin );
		dest.writeLong( amountout );
	}
	
	public static final Parcelable.Creator<Transaction> CREATOR
    	= new Parcelable.Creator<Transaction>() {
			public Transaction createFromParcel(Parcel in) {
				return new Transaction(in);
			}

			public Transaction[] newArray(int size) {
				return new Transaction[size];
			}
	};

	@Override
	public int compareTo(Transaction another)
	{
		return another.date.compareTo(date);
	}
}

class Key
{
	public String hash;
	public int hit;
	
	public Key( String hash )
	{
		this.hash = hash;
		this.hit = 0;
	}
	
	public Key( String hash, int hit)
	{
		this.hash = hash;
		this.hit = hit;
	}
	
	public static boolean arrayContains( Key [] keys, String hash )
	{
		for ( Key key : keys)
		{
			if ( key.hash.equals(hash))
			{
				key.hit = 1;
				return true;
			}
		}
		return false;
	}
}

class Wallet
{
	public String name;
	public long balance;
	public Key [] keys;
	public Date lastUpdated;
	public Transaction [] transactions;
	private int version;
	private WalletActivity activity;
	
	private final static int LEGACY_VERSION = 1;
	private final static int TRANSACTIONS_ADDED_VERSION = 2;
	private final static int CURRENT_VERSION = 2;

	public void notifyUser()
	{
		/** code not ready yet
		if ( activity != null )
		{
			long when = System.currentTimeMillis();
			Notification n = new Notification( android.R.drawable.stat_notify_more, "Balance", when);
	
			NotificationManager nm = (NotificationManager) activity.getSystemService( Context.NOTIFICATION_SERVICE );
			Context globalContext = activity.getApplicationContext();
			
			Intent notificationIntent = new Intent( activity, activity.getClass() );
			PendingIntent contentIntent = PendingIntent.getActivity( activity, 0, notificationIntent, 0 );
			
			n.setLatestEventInfo( globalContext, "New Balance", "New Balance", contentIntent );
			nm.notify(1, n);
		}
		*/
	}

	protected void SaveWallet( PrintWriter out ) throws IOException
	{
		version = CURRENT_VERSION;

		if ( out != null )
		{
			out.println("walletversion");
			out.println(version);
			out.println(name);
			out.println(balance);
			out.println(lastUpdated.getTime());
			
			if ( transactions == null )
			{
				out.println(0);
			}
			else
			{
				out.println( transactions.length );
				
				for ( Transaction t : transactions )
				{
					t.saveTransaction( out );
				}
			}
	
			for (Key key : keys )
			{
				out.println(key.hash);
				out.println(key.hit);
			}
		}
	}

	private boolean fillFromReader( BufferedReader bin ) throws IOException, ParseException
	{
		ArrayList<Key> keysArray = new ArrayList<Key>();
		String keyHash;
		boolean foundKey = false;

		while ( ( keyHash = bin.readLine() ) != null )
		{
			// add a space at the start to make the regex work more easily
			keyHash = " " + keyHash;

			Pattern p = Pattern.compile( "\\W(1[1-9A-HJ-NP-Za-km-z]{27,34})" );
			Matcher m = p.matcher( keyHash );

			if ( m.find() )
			{
				Log.d("balance", "Key " + m.group(1) + " found");
				keysArray.add( new Key( m.group( 1 ) ) );
				foundKey = true;
			}
		}
		
		if ( foundKey )
		{
			keys = new Key[ keysArray.size() ];
			keysArray.toArray( keys );
			lastUpdated = new Date();
			balance = 0;
		}
		
		return foundKey;
	}

	public Wallet(String name, File file, WalletActivity activity ) throws IOException
	{
		this.activity = activity;
		this.name = name;
		boolean foundKey = false;
		BufferedReader in = new BufferedReader( new FileReader( file ) );
		
		foundKey = fillFromReader( in );

		in.close();

		if ( !foundKey )
		{
			throw new ParseException("Did not find any keys");
		}
	}

	public Wallet(String name, URI url, WalletActivity activity ) throws IOException, ParseException
	{
		this.activity = activity;
		this.name = name;
		HttpClient client = new DefaultHttpClient();
		HttpGet hg = new HttpGet( url );
		boolean foundKey = false;

		HttpResponse resp;
		resp = client.execute( hg );
		if ( resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK )
		{
			HttpEntity entity = resp.getEntity();
			
			if ( entity != null )
			{
				foundKey = fillFromReader( new BufferedReader( new InputStreamReader( entity.getContent() ) ) );
			}
			else
			{
				throw new ParseException("No response body");
			}
		}
		else
		{
			throw new ParseException("Did not get 200 OK");
		}
		
		if ( !foundKey )
		{
			throw new ParseException("Did not find any keys");
		}
	}

	protected Wallet( BufferedReader in, WalletActivity activity ) throws IOException, NumberFormatException
	{
		this.activity = activity;
		name = in.readLine();
		if ( name.equals("walletversion") )
		{
			version = Integer.parseInt( in.readLine());
			name = in.readLine();
		}
		else
		{
			version = LEGACY_VERSION;
		}
		balance = Long.parseLong( in.readLine() );
		lastUpdated = WalletActivity.myParseDate( in.readLine() );
		
		if ( version >= TRANSACTIONS_ADDED_VERSION )
		{
			int numTx = Integer.parseInt( in.readLine() );
			transactions = new Transaction[ numTx ];
			for (int i = 0; i < numTx; i++)
			{
				transactions[i] = new Transaction( in );
			}
		}

		ArrayList<Key> keysArray = new ArrayList<Key>();
		String keyHash = null;

		while ( ( keyHash = in.readLine() ) != null )
		{
			int hit = Integer.parseInt( in.readLine() );
			keysArray.add( new Key( keyHash, hit ) );
		};
		
		keys = new Key[ keysArray.size()];
		keysArray.toArray( keys );
	}
	
	public static Wallet [] getStoredWallets( Context context, WalletActivity activity ) throws IOException
	{
		ArrayList<Wallet> walletsArray = new ArrayList<Wallet>();

		String [] files = context.fileList();
		
		if ( files.length == 0 )
			return null;
		
		for (String filename : files)
		{
			BufferedReader in = new BufferedReader( new InputStreamReader( context.openFileInput( filename ) ) );
			try
			{
				walletsArray.add( new Wallet( in, activity ) );
			}
			catch (NumberFormatException e)
			{
			}
			in.close();
		}
		Wallet [] wallets = new Wallet[ walletsArray.size() ];
		walletsArray.toArray( wallets );
		return wallets;
	}
	
	public static void saveWallets( Wallet [] wallets, Context context) throws IOException
	{
		for (Wallet w : wallets)
		{
			PrintWriter out = new PrintWriter( new OutputStreamWriter( context.openFileOutput( w.name, Context.MODE_PRIVATE ) ) );
			w.SaveWallet( out );
			out.close();
		}
	}
	
	public int getActiveKeyCount()
	{
		int count = 0;
		
		for (Key k : keys)
		{
			if ( k.hit > 0 )
				count++;
		}

		if ( count == 0 )
			count = keys.length;

		return count;
	}
}


class WalletAdapter extends BaseAdapter
{
	private Wallet [] wallets;
	private WalletActivity context;
	
	public WalletAdapter( WalletActivity c, Wallet [] w)
	{
		this.wallets = w;
		this.context = c;
	}

	@Override
	public int getCount()
	{
		return wallets.length;
	}

	@Override
	public Object getItem(int position)
	{
		return wallets[position];
	}

	@Override
	public long getItemId(int position)
	{
		return 0;
	}

	private String getTimeStampString( Date d )
	{
		String result = DateFormat.getDateFormat( context ).format( d );
		
		result += " " + DateFormat.format("h:mmaa", d ).toString();
		
		return result;
	}
	
	@Override
	public View getView(int position, View convertView, ViewGroup parent)
	{
		double exchrate = Currency.getRate( context.getActiveCurrency() );

		LayoutInflater inflater = LayoutInflater.from( context );
		
		View v = inflater.inflate( R.layout.walletlayout, null );
		v.setLongClickable( true );
		v.setOnClickListener( context );
		v.setTag(position);
		
		DecimalFormat df = new DecimalFormat("0.00");

		TextView balanceTextView = (TextView) v.findViewById(R.id.walletBalanceText);
		balanceTextView.setText(df.format( wallets[position].balance / BalanceRetriever.SATOSHIS_PER_BITCOIN ) );
		
		TextView curTextView = (TextView) v.findViewById(R.id.walletCurText);
		curTextView.setTextSize( 10 );

		if ( exchrate != 0)
		{
			curTextView.setText( "(" + df.format( wallets[position].balance * exchrate / BalanceRetriever.SATOSHIS_PER_BITCOIN ) + context.getActiveCurrency() + ")" );
		}
		else
		{
			curTextView.setText( "" );		
		}

		balanceTextView.setTextSize( 20 );
		balanceTextView.setTextColor( Color.GREEN );
		
		TextView nameTextView = (TextView) v.findViewById(R.id.walletNameText );
		nameTextView.setText(wallets[position].name);
		nameTextView.setTextColor( Color.BLACK );
		nameTextView.setTextSize( 16 );
		
		TextView lastUpdatedTextView = (TextView) v.findViewById(R.id.lastUpdatedText );
		
		lastUpdatedTextView.setTextColor( Color.GRAY );
		lastUpdatedTextView.setTextSize( 8 );
		lastUpdatedTextView.setText( "Last Updated: " + getTimeStampString( wallets[position].lastUpdated ) );

		TextView infoTextView = (TextView) v.findViewById(R.id.infoText );
		
		infoTextView.setTextColor( Color.GRAY );
		infoTextView.setTextSize( 8 );
		infoTextView.setText( wallets[position].keys.length + " keys (" + wallets[position].getActiveKeyCount() +" in use)" );
		
		TextView txLastUpdatedTextView = (TextView) v.findViewById(R.id.txLastUpdatedText );
		txLastUpdatedTextView.setTextColor( Color.GRAY );
		txLastUpdatedTextView.setTextSize( 8 );
		
		TextView txInfoTextView = (TextView) v.findViewById(R.id.txInfoText);
		txInfoTextView.setTextColor( Color.GRAY );
		txInfoTextView.setTextSize( 8 );
		
		if ( wallets[position].transactions != null && wallets[position].transactions.length > 0 )
		{
			txLastUpdatedTextView.setText( "Last Transaction: " + getTimeStampString( Transaction.latest( wallets[position].transactions ) ) );
			txInfoTextView.setText( wallets[position].transactions.length + " transactions ("+Transaction.compressTransactions(wallets[position].transactions).length+" unique)" );
		}
		else
		{
			txLastUpdatedTextView.setText("");
			txInfoTextView.setText("");
		}

		Button button = (Button) v.findViewById(R.id.updateButton);
		button.setTag(position);
		button.setOnClickListener(context);
		return v;
	}
}

public class WalletActivity extends Activity implements OnClickListener
{
	public final static String TRANSACTIONS = "net.phase.wallet.transactions";
	public final static String WALLETNAME = "net.phase.wallet.name";
	public final static String TRANSACTIONSTYLE = "net.phase.wallet.transactionsstyle";
	private ProgressDialog dialog;
	private Wallet[] wallets;
	private int nextWallet = -1;
	private static final int DIALOG_URL	= 1;
	private static final int DIALOG_FILE = 2;
	private static final int DIALOG_OPTIONS = 3;
	private String activeCurrency = "USD";
	private static Context context;

	public static Date myParseDate( String line )
	{
		Date d = null;

		// make guesses - if it contains a space, it's probably the output of date.toString()
		if ( line.indexOf(' ') == -1)
		{
			try
			{
				d = new Date( Long.parseLong( line ) );
			}
			catch (NumberFormatException e )
			{
				
			}
		}
		else
		{
			try
			{
				d = new Date( line );
			}
			catch ( IllegalArgumentException e )
			{
			}
		}
		
		if ( d == null )
		{
			// set it to 1970... not much we can do here
			d = new Date( 0 );
		}

		return d;
	}

	private boolean savePreferences()
	{
		SharedPreferences pref = getPreferences( MODE_PRIVATE );
		
		SharedPreferences.Editor editor = pref.edit();
		editor.putString("currency", activeCurrency );
		return editor.commit();
	}

	private void loadPreferences()
	{
		SharedPreferences pref = getPreferences( MODE_PRIVATE );
		setActiveCurrency( pref.getString("currency", "USD" ) );
	}

	public static void toastMessage( String message )
	{
		CharSequence text = message;
		int duration = Toast.LENGTH_SHORT;

		if ( context != null )
		{
			Toast toast = Toast.makeText(context, text, duration);
			toast.show();
		}
	}

	/** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        WalletActivity.context = getApplicationContext(); 
        loadPreferences();
        try
        {
        	wallets = Wallet.getStoredWallets( this, this );
        }
        catch (IOException e)
        {
        	toastMessage(e.getMessage());
        }
        
        Currency c = new Currency( currencyHandler );
        Thread t = new Thread( c );
        t.start();

        updateWalletList();
    }

    public void addWallet( Wallet wallet )
    {
    	if ( wallets == null )
    	{
    		wallets = new Wallet[1];
    		wallets[0] = wallet;
    	}
    	else
    	{
        	Wallet [] newWallets = new Wallet[ wallets.length + 1 ];
        	int i = 0;
        	for (i = 0; i < wallets.length; i++ )
        	{
        		newWallets[i] = wallets[i];
        		if ( newWallets[i].name.equals( wallet.name ) )
        		{
        			// name clash!
        			wallet.name = wallet.name.concat("2");
        		}
        	}
        	newWallets[i] = wallet;
        	wallets = newWallets;
    	}
    	updateWalletList();
    }

    public void updateWalletList()
    {
        if ( wallets != null )
        {
            setContentView( R.layout.main );
            ListView view = (ListView) findViewById( R.id.walletListView );

            WalletAdapter adapter = new WalletAdapter( this, wallets );
	        view.setAdapter( adapter );
	        registerForContextMenu( view );
	        try
	        {
	        	Wallet.saveWallets( wallets, this );
	        }
	        catch (IOException e)
	        {
	        	toastMessage("Unable to save wallet data " + e.getMessage());
	        }
        }
        else
        {
        	setContentView( R.layout.mainnowallets );
        }
    }
    
    private void updateAll()
    {
    	if ( wallets != null )
    	{
	    	if ( wallets.length > 0 )
	    	{
	    		nextWallet = 1;
	    	}
	
	    	updateWalletBalance(wallets[0], true );
    	}
    	else
    	{
    		toastMessage("No wallets to update!");
    	}
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu)
    {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.mainmenu, menu);
        return true;
    }
    
    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo )
    {
    	super.onCreateContextMenu(menu, v, menuInfo);

    	MenuInflater inflater = getMenuInflater();
    	inflater.inflate(R.menu.walletmenu, menu );
    }

    FileFilter fileFilter = new FileFilter()
    {
		@Override
		public boolean accept(File pathname)
		{
			if ( pathname.getName().contains("key") && 
				 pathname.getName().endsWith(".txt") )
			{
				return true;
			}
			return false;
		}
    };

    protected void onPrepareDialog( int id, Dialog dialog )
    {
    	switch ( id )
    	{
	    	case DIALOG_URL:
		    	TextView tv = (TextView) dialog.findViewById(R.id.pasteBinHelpText);
		    	tv.setMovementMethod(LinkMovementMethod.getInstance());
		
		    	break;
	    	case DIALOG_FILE:
		    	TextView tvhelp = (TextView) dialog.findViewById(R.id.helpText);
		    	tvhelp.setMovementMethod(LinkMovementMethod.getInstance());
		
		    	Spinner fileSpinner = (Spinner) dialog.findViewById(R.id.fileInput);
		    	
		    	File [] files = Environment.getExternalStorageDirectory().listFiles( fileFilter );
		    	
		    	if ( files == null || files.length == 0 )
		    	{
		    		toastMessage("No files found on sdcard");
		    	}
		    	else
		    	{
			    	ArrayAdapter<File> adapter = new ArrayAdapter<File>( this, android.R.layout.simple_spinner_item, files );
			    	fileSpinner.setAdapter( adapter );
		    	}			    	
		    	break;
	    	case DIALOG_OPTIONS:
		    	Spinner currencySpinner = (Spinner) dialog.findViewById(R.id.currencySpinner);
		    	String current = getActiveCurrency();
		    	String [] currencies = { current };

		    	try
		    	{
		    		currencies = Currency.getAvailableCurrencies();
		    	}
		    	catch ( IOException e )
		    	{
		    		WalletActivity.toastMessage("Warning: unable to obtain currency information");
		    	}
		    	catch ( JSONException e )
		    	{
		    	}
		    	ArrayAdapter<String> adapter = new ArrayAdapter<String>( this, android.R.layout.simple_spinner_item, currencies );
		    	currencySpinner.setAdapter( adapter );
		    	for ( int i = 0 ; i < currencies.length; i++ )
		    	{
		    		if ( currencies[i].equals( current ) )
		    		{
		    			currencySpinner.setSelection( i );
		    			break;
		    		}
		    	}

	    		break;
    	}
    }

    private void showTransactions( Wallet wallet )
    {
    	showTransactions( wallet, TransactionAdapter.STYLE_NORMAL );
    }

    private void showTransactions( Wallet wallet, int style )
    {
			Intent intent = new Intent(this, TransactionsActivity.class);
			intent.putExtra( WALLETNAME, wallet.name);
			intent.putExtra( TRANSACTIONSTYLE, style );
			Arrays.sort( wallet.transactions );

			if ( style == TransactionAdapter.STYLE_NORMAL )
			{
				Transaction [] compressedTransactions = Transaction.compressTransactions(wallet.transactions);
				intent.putExtra( TRANSACTIONS, compressedTransactions );
			}
			else
			{
				intent.putExtra( TRANSACTIONS, wallet.transactions );
			}

   			startActivity(intent);
    }

    protected Dialog onCreateDialog( int id )
    {
    	AlertDialog.Builder builder;
    	AlertDialog alertDialog;
    	builder = new AlertDialog.Builder( this );
    	LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
    	View layout = null;
    	final WalletActivity parentActivity = this;
    	
    	switch ( id )
    	{
	    	case DIALOG_URL:
		    	layout = inflater.inflate(R.layout.urlfetch_dialog, null );
		    	
		    	TextView tv = (TextView) layout.findViewById(R.id.pasteBinHelpText);
		    	tv.setMovementMethod(LinkMovementMethod.getInstance());
		
		    	final EditText hashEditText = (EditText) layout.findViewById(R.id.hashEditText);
		    	final EditText nameEditText = (EditText) layout.findViewById(R.id.nameEditText);
		
		    	builder.setView(layout);
		    	builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
		            public void onClick(DialogInterface dialog, int id) {
		                 String hash = hashEditText.getText().toString();
		                 String name = nameEditText.getText().toString();
		                 
		                 if ( !hash.startsWith("http") )
		                 {
		                	 hash = "http://pastebin.com/raw.php?i=" + hash;
		                 }
		
		                 try
		                 {
		                	 Wallet w = new Wallet(name, new URI( hash ), parentActivity );
		                	 addWallet( w );
		                 }
		                 catch (Exception e)
		                 {
		                	 toastMessage(e.getMessage());
		                 }
		            }
		        });
		    	builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
		           public void onClick(DialogInterface dialog, int id) {
		                dialog.cancel();
		           }
		       });
		    	break;
	    	case DIALOG_FILE:
		    	layout = inflater.inflate(R.layout.file_dialog, null );
		    	
		    	TextView tvhelp = (TextView) layout.findViewById(R.id.helpText);
		    	tvhelp.setMovementMethod(LinkMovementMethod.getInstance());
		
		    	final Spinner fileSpinner = (Spinner) layout.findViewById(R.id.fileInput);
		    	final EditText nameEditText2 = (EditText) layout.findViewById(R.id.nameEditText);
		    	
		    	builder.setView(layout);
		    	builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
		            public void onClick(DialogInterface dialog, int id) {
		                 File filename = (File) fileSpinner.getSelectedItem();
		                 String name = nameEditText2.getText().toString();
		                 
		                 try
		                 {
		                	 Wallet w = new Wallet(name, filename, parentActivity );
		                	 addWallet( w );
		                 }
		                 catch (Exception e)
		                 {
		                	 toastMessage(e.getMessage());
		                 }
		            }
		        });

		    	builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
		           public void onClick(DialogInterface dialog, int id) {
		                dialog.dismiss();
		           }
		       });
		    	break;
	    	case DIALOG_OPTIONS:
		    	layout = inflater.inflate(R.layout.options_dialog, null );
		    	
		    	final Spinner currencySpinner = (Spinner) layout.findViewById(R.id.currencySpinner);
		
		    	builder.setView(layout);
		    	builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
		            public void onClick(DialogInterface dialog, int id) {
		                 String currency = (String) currencySpinner.getSelectedItem();
		                 setActiveCurrency( currency );
		                 savePreferences();
		            }
		        });
		    	builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
		           public void onClick(DialogInterface dialog, int id) {
		                dialog.cancel();
		           }
		       });
		    	break;

    	}

    	alertDialog = builder.create();

    	return alertDialog;
    }

    @Override
    public boolean onContextItemSelected(MenuItem item)
    {
    	AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();

    	switch (item.getItemId())
    	{
    		case R.id.updateItem:
    			updateWalletBalance( wallets[info.position], false );
    			return true;
       		case R.id.removeItem:
    			removeWallet( wallets[info.position].name );
    			updateWalletList();
    			return true;
       		case R.id.viewItem:
       			if ( wallets[info.position].transactions == null )
       			{
       				toastMessage("Please update wallet first");
       			}
       			else
       			{
       				showTransactions( wallets[info.position] );
       			}
       			return true;
    		default:
    			return super.onOptionsItemSelected(item);
    	}
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item)
    {
    	switch (item.getItemId())
    	{
    		case R.id.importURL:
    			showDialog( DIALOG_URL );
    			return true;
    		case R.id.importFile:
    			showDialog( DIALOG_FILE );
    			return true;
    		case R.id.updateAllItem:
    			updateAll();
    			return true;
    		case R.id.optionsItem:
    			showDialog( DIALOG_OPTIONS );
    			return true;
    		case R.id.helpItem:
    			Uri uri = Uri.parse( "http://code.google.com/p/bitcoinwallet/wiki/Using" );
    			startActivity( new Intent( Intent.ACTION_VIEW, uri ) );
    			return true;
    		default:
    			return super.onOptionsItemSelected(item);
    	}
    }

    public Thread updateWalletBalance( Wallet w, boolean fast)
    {
    	BalanceRetriever br = new BalanceRetriever( progressHandler, w, fast );

    	dialog = new ProgressDialog( this );
    	dialog.setMessage( "Obtaining balance ("+w.name+")..." );
    	dialog.setProgressStyle( ProgressDialog.STYLE_HORIZONTAL );
    	if ( fast )
    		dialog.setMax( w.getActiveKeyCount() );
    	else
    		dialog.setMax( w.keys.length );
    	dialog.setProgress( 0 );
    	dialog.show();
    	
    	Thread t = new Thread( br );
    	t.start();
    	
    	return t;
    }

    public void removeWallet( String name )
    {
    	Wallet [] newWallets = null;

    	if ( wallets.length == 1 )
    	{
    		wallets = null;
    		updateWalletList();
    	}
    	else
    	{
	    	newWallets = new Wallet[ wallets.length - 1];
	    	int i = 0;
	    	for (Wallet wallet : wallets )
	    	{
	    		if (!wallet.name.equals(name))
	    			newWallets[i++] = wallet;
	    	}
    	}
    	// delete the wallet file
    	deleteFile( name );
    	wallets = newWallets;
    }

    Handler currencyHandler = new Handler() {
    	public void handleMessage( Message msg )
    	{
			updateWalletList();
    	}
    };

    Handler progressHandler = new Handler() {
    	public void handleMessage( Message msg )
    	{
    		switch ( msg.what )
    		{
    			case BalanceRetriever.MESSAGE_UPDATE:
    				dialog.incrementProgressBy( 1 );
    				break;
    			case BalanceRetriever.MESSAGE_FINISHED:
    				dialog.dismiss();
    				switch (msg.arg1)
    				{
    					case BalanceRetriever.MESSAGE_STATUS_SUCCESS:
    						// code to refresh all wallets
    						if ( nextWallet > 0 && nextWallet < wallets.length )
    						{
    							updateWalletBalance(wallets[nextWallet], false );
    							nextWallet++;
    						}
    						else
    						{
        						updateWalletList();

        						// send an update to the widget
        				        Intent intent = new Intent( WalletWidgetProvider.UPDATE_WIDGET );
        				        intent.setClassName( "net.phase.wallet", "net.phase.wallet.WalletWidgetProvider" );
        				        sendBroadcast( intent );

    							nextWallet = -1;
    						}
    						break;
    					case BalanceRetriever.MESSAGE_STATUS_NETWORK:
    						toastMessage("Network error");
    						break;
    					case BalanceRetriever.MESSAGE_STATUS_NOKEYS:
    						toastMessage("No keys at that location");
    						break;
    					case BalanceRetriever.MESSAGE_STATUS_JSON:
    						toastMessage("JSON Parse Error");
    						break;
    					case BalanceRetriever.MESSAGE_STATUS_PARSE:
    						toastMessage("Parse Error");
    						break;
    				}
    				break;
    			case BalanceRetriever.MESSAGE_SETLENGTH:
    				dialog.setMax( msg.arg1 );
    				break;
    		}
    	}
    };

	@Override
	public void onClick(View v)
	{
		int i = (Integer) v.getTag();

		if ( v.getId() == R.id.updateButton )
		{
			updateWalletBalance( wallets[ i ], true );
		}
		else
		{
   			if ( wallets[i].transactions == null )
   			{
   				toastMessage("Please update wallet");
   			}
   			else
   			{
   				showTransactions(wallets[i]);
   			}
   		}
	}
	
	public String getActiveCurrency()
	{
		return activeCurrency;
	}
	
	public void setActiveCurrency( String currency )
	{
		activeCurrency = currency;
		updateWalletList();
	}
}

class tx
{
	public String txhash;
	public int rec;
	public long value;
	public String inKeyHash;
	public String outKeyHash;
	
	public tx( String txhash, int rec, long value, String outKeyHash, String inKeyHash )
	{
		// value is number of satoshis
		this.txhash = txhash;
		this.rec = rec;
		this.value = value;
		this.outKeyHash = outKeyHash;
		this.inKeyHash = inKeyHash;
	}
}

class prevout
{
	public String prevTxHash;
	public String txhash;
	public int rec;
	public Date date;
	public String addr;
	
	public prevout( String txhash, String prevTxHash, int rec, Date date, String addr )
	{
		this.txhash = txhash;
		this.prevTxHash = prevTxHash;
		this.rec = rec;
		this.date = date;
		this.addr = addr;
	}
}

class BalanceRetriever implements Runnable
{
	public static final int MESSAGE_UPDATE = 1;
	public static final int MESSAGE_FINISHED = 2;
	public static final int MESSAGE_SETLENGTH = 3;
	
	public static final int MESSAGE_STATUS_SUCCESS = 0;
	public static final int MESSAGE_STATUS_NOKEYS = 1;
	public static final int MESSAGE_STATUS_NETWORK = 2;
	public static final int MESSAGE_STATUS_JSON= 3;
	public static final int MESSAGE_STATUS_PARSE = 4;
	public static final int MESSAGE_STATUS_MISSING_TX = 5;
	

	public static final double SATOSHIS_PER_BITCOIN = 100000000.0;
	private static final String baseUrl = "http://blockexplorer.com/q/mytransactions/";

	// number of transactions that can be queried from blockexplorer in each GET request
	// this is limited by the maximum length of a GET request
	private static final int MAX_LENGTH = 50;
	// balance is number of microbitcoins (a millionth of a bitcoin)
	private long balance;
	private Handler updateHandler;
	private Wallet wallet;
	private boolean fast;
	
	private ArrayList<String> transactionCache;
	private ArrayList<Transaction> transactions;
	private ArrayList<tx> txs;
	private ArrayList<prevout> pendingDebits;
	private static final java.text.DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	
	public long getFinalBalance()
	{
		return this.balance;
	}
	
	public int getNumberOfKeys()
	{
		return wallet.keys.length;
	}

	public BalanceRetriever( Handler updateHandler, Wallet wallet, boolean fast )
	{
		this.balance = 0;
		this.updateHandler = updateHandler;
		this.wallet = wallet;
		this.fast = fast;

		transactions = new ArrayList<Transaction>();
		transactionCache = new ArrayList<String>();
		txs = new ArrayList<tx>();
		pendingDebits = new ArrayList<prevout>();
	}

	private tx getMatchingTx( String txhash, int rec )
	{
		for (tx theTx : txs)
		{
			if ( theTx.txhash.equals( txhash ) &&
				 theTx.rec == rec )
			{
				return theTx;
			}
		}
//		Log.e("balance", "Could not find hash " + hash + ":" + rec );
		return null;
	}
			
	private tx getFirstNonMatchingTx( String txhash, Key [] keys )
	{
		for (tx theTx : txs)
		{
			if ( theTx.txhash.equals(txhash) && 
				 !Key.arrayContains(keys, theTx.outKeyHash ) )
			{
				return theTx;
			}
		}
		return null;
	}
	
	public void run()
	{
		balance = 0;
		int i = 0;
		StringBuffer url = new StringBuffer( baseUrl );
		int status = 0;
		boolean fastKeyFound = false;
		
		for (Key k : wallet.keys )
		{
			if ( !fast )
			{
				k.hit = 0;
			}
			else
			{
				if ( k.hit > 0 )
				{
					fastKeyFound = true;
				}
			}
		}
		
		// if we asked for fast, but no fast keys found, just override this
		if ( fast && !fastKeyFound )
		{
			fast = false;
		}

		try
		{
			int numberOfKeys = wallet.keys.length;

			if ( fast )
			{
				numberOfKeys = wallet.getActiveKeyCount();
			}

			updateHandler.sendMessage( updateHandler.obtainMessage(MESSAGE_SETLENGTH, numberOfKeys, 0 ) );
			updateHandler.sendMessage( updateHandler.obtainMessage(MESSAGE_UPDATE ) );
	
			for ( Key key : wallet.keys)
			{
				if ( ( i % MAX_LENGTH ) == (MAX_LENGTH - 1) )
				{
					updateBalanceFromUrl( url.substring(0, url.length() - 1 ) );
	
					url = new StringBuffer( baseUrl );
				}
	
				if ( !fast || key.hit > 0 )
				{
					url.append( key.hash );
					url.append('.');
					i++;
				}
				updateHandler.sendMessage( updateHandler.obtainMessage(MESSAGE_UPDATE) );
			}
			
			updateBalanceFromUrl( url.substring(0, url.length() - 1 ) );

			updateHandler.sendMessage( updateHandler.obtainMessage(MESSAGE_UPDATE) );
	
			// look through previous transactions and debit payments
			for ( prevout previousOut : pendingDebits )
			{
				tx matchingTx = getMatchingTx( previousOut.prevTxHash, previousOut.rec );
				
				if ( matchingTx != null )
				{
					balance -= matchingTx.value;
				}
				else
				{
					// not sure how this can happen, but it happened once for a user, so handle it here
				}
				
				tx outputTx = getFirstNonMatchingTx( previousOut.txhash, wallet.keys );
				if ( outputTx != null)
				{
					transactions.add( new Transaction( previousOut.date, -matchingTx.value, previousOut.addr, outputTx.outKeyHash ) );
				}
				else
				{
					transactions.add( new Transaction( previousOut.date, -matchingTx.value, previousOut.addr, "unknown" ) );
				}
			}

			status = MESSAGE_STATUS_SUCCESS;			
		}
		catch (IOException e)
		{
			status = MESSAGE_STATUS_NETWORK;			
		}
		catch (JSONException e)
		{
			status = MESSAGE_STATUS_JSON;
		}
		catch (java.text.ParseException e)
		{
			status = MESSAGE_STATUS_PARSE;
		}

		updateHandler.sendMessage( updateHandler.obtainMessage(MESSAGE_FINISHED, status, 0 ) );
		
		if ( status == MESSAGE_STATUS_SUCCESS )
		{
			if ( wallet.balance != 0 &&
				 wallet.balance != balance )
			{
				wallet.notifyUser();
			}
			wallet.balance = balance;
			wallet.lastUpdated = new Date();
			wallet.transactions = new Transaction[transactions.size()];
			transactions.toArray( wallet.transactions );
		}
	}

	private void updateBalanceFromUrl( String url ) throws IOException, JSONException, java.text.ParseException
	{
//		Log.i("balance", "fetching URL "+ url);
		HttpClient client = new DefaultHttpClient();
		HttpGet hg = new HttpGet( url );

		HttpResponse response = client.execute(hg);
		
		if ( response.getStatusLine().getStatusCode() == HttpStatus.SC_OK )
		{
			JSONObject resp = new JSONObject( EntityUtils.toString( response.getEntity() ) );
			
			// JSONObject.keys() returns Iterator<String> but for some reason
			// isn't typed that way
			@SuppressWarnings("unchecked")
			Iterator<String> itr = resp.keys();
			
			// look through every transaction
			while ( itr.hasNext() )
			{
				JSONObject txObject = resp.getJSONObject( itr.next() );
				String txHash = txObject.getString("hash");
				String inKeyHash = "unknown";
				
				// only process transaction if we haven't seen it before
				if ( !transactionCache.contains( txHash ) )
				{
//					Log.i("balance", "Parsing txObject " + txHash );
					transactionCache.add( txHash );
					// find the in transaction
					JSONArray txsIn = txObject.getJSONArray("in");
					Date date = formatter.parse( txObject.getString("time") );
					
					for ( int i = 0; i < txsIn.length(); i++ )
					{
						JSONObject inRecord = txsIn.getJSONObject( i );
						try
						{
							inKeyHash = inRecord.getString("address");
	
							// if one of our keys is there, we are paying :(
							if ( Key.arrayContains(wallet.keys, inKeyHash ) )
							{
								JSONObject prevRecord = inRecord.getJSONObject("prev_out");
								// if we paid for part of this transaction, record this.
								pendingDebits.add( new prevout( txHash, prevRecord.getString("hash"), prevRecord.getInt("n"), date, inKeyHash ) );
							}
						}
						catch ( JSONException e )
						{
							// no address.  Probably a generation transaction
						}
					}

					// find the out transaction
					JSONArray txsOut = txObject.getJSONArray("out");
					
					for ( int i = 0; i < txsOut.length(); i++ )
					{
						JSONObject outRecord = txsOut.getJSONObject( i );
						String outKeyHash = outRecord.getString("address");
						// convert to microbitcoins for accuracy
						long value = (long) ( outRecord.getDouble( "value" ) * SATOSHIS_PER_BITCOIN );
						// store the out transaction, this is used later on
						txs.add( new tx( txHash, i, value, outKeyHash, inKeyHash ) );

						// if one of our keys is there, add the balance
						if ( Key.arrayContains(wallet.keys, outKeyHash ) )
						{
							transactions.add( new Transaction( date, value, inKeyHash, outKeyHash ) );
							balance += value;
						}
					}
				}
			}
		}
	}
}
